Python: add agent-framework-hosting-responses channel#5639
Python: add agent-framework-hosting-responses channel#5639eavanvalkenburg wants to merge 2 commits intomicrosoft:feature/python-hostingfrom
Conversation
200aa2a to
6f8cf42
Compare
New ``agent-framework-hosting`` package implementing ADR 0026 / SPEC-002:
the channel-neutral host that lets a single ``Agent`` (or ``Workflow``)
fan out across multiple wire protocols ("channels") behind one Starlette
ASGI app.
Surface (re-exported from ``agent_framework_hosting``):
- ``AgentFrameworkHost`` — wraps a hostable target, mounts channels onto
an ASGI app, owns per-isolation-key ``AgentSession`` reuse, threads
request context (``response_id`` / ``previous_response_id``) into
context providers via an ``ExitStack`` of ``bind_request_context``
calls, and exposes an opt-in Hypercorn ``serve()`` helper (extra
``[serve]``).
- ``Channel`` protocol + ``ChannelContribution`` — the surface a channel
package implements (routes, lifespans, identity hooks, …).
- ``ChannelRequest`` / ``ChannelSession`` / ``ChannelIdentity`` /
``ChannelPush`` / ``ChannelCommand[Context]`` / ``ChannelRunHook`` /
``ChannelStreamTransformHook`` / ``DeliveryReport`` /
``HostedRunResult`` / ``ResponseTarget`` / ``ResponseTargetKind`` /
``apply_run_hook`` — channel-side dataclasses + helpers.
- ``IsolationKeys`` + ``ISOLATION_HEADER_USER`` / ``..._CHAT`` +
``get/set/reset_current_isolation_keys`` — the host's ASGI middleware
reads the ``x-agent-{user,chat}-isolation-key`` headers off each
inbound request and exposes them to the agent stack via a
``ContextVar`` so storage-side providers (e.g.
``FoundryHostedAgentHistoryProvider``) can apply per-tenant
partitioning without channels having to forward anything.
Includes 45 unit tests covering the host, channel contributions,
isolation contextvar, and shared types. Registers the package in
``python/pyproject.toml`` ``[tool.uv.sources]`` and adds the matching
pyright ``executionEnvironments`` entry for tests.
Hypercorn is an optional dependency (``[serve]`` extra); the soft import
in ``serve()`` is annotated for pyright since it isn't on the default
install.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New ``agent-framework-hosting-responses`` package implementing the
OpenAI Responses-shaped HTTP channel for the Hosting framework. Mounts
``POST /responses`` (and a ``/responses/{response_id}`` GET) onto an
``AgentFrameworkHost`` and translates the OpenAI Responses wire shape
to/from the channel-neutral ``ChannelRequest`` / ``HostedRunResult``
plumbing.
Surface (re-exported from ``agent_framework_hosting_responses``):
- ``ResponsesChannel`` -- concrete ``Channel`` implementation. Owns the
Starlette route(s), parses inbound JSON into ``ChannelRequest``, runs
the optional ``ChannelRunHook``, calls back into the
``ChannelContext`` to invoke the agent target, builds Responses
envelopes (sync JSON or SSE), and respects
``DeliveryReport.include_originating`` so cross-channel push routes
only ack to the originating Responses caller.
- The minted ``response_id`` is propagated via the host's ContextVar
machinery so storage-side history providers (e.g.
``FoundryHostedAgentHistoryProvider``) persist envelopes against the
same id the channel returns.
- 48 unit tests covering route wiring, parsing of each Responses input
shape, hook composition, sync vs streaming paths, and originating
vs non-originating delivery branches.
Registers the package in ``python/pyproject.toml`` ``[tool.uv.sources]``
and adds the matching pyright ``executionEnvironments`` entry.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
6f8cf42 to
ee183ae
Compare
| # user-iso companion header is consumed at the host level by | ||
| # ``_FoundryIsolationASGIMiddleware`` so the channel never has | ||
| # to import Foundry-specific types. | ||
| chat_iso = request.headers.get("x-agent-chat-isolation-key") |
There was a problem hiding this comment.
Should we be trusting this header straight off the wire? The comment above says it is "platform-injected" by Foundry, but the channel itself does no validation. If ResponsesChannel is mounted anywhere outside Foundry, or in front of the Foundry middleware in the ASGI stack, any HTTP caller can set x-agent-chat-isolation-key: <someone-else> and end up sharing that user's per-conversation session bucket. Right? Should we have, at minimum, a sanity check that a host-level guard is enforced before this point, and probably a docstring note that mounting this channel requires the isolation middleware in front?
| # ``previous_response_id`` and the storage chain walks. We pass | ||
| # both anchors via ``ChannelRequest.attributes`` so the host | ||
| # can pick them up without a channel-specific contract. | ||
| previous_response_id: str | None = None |
There was a problem hiding this comment.
The docstring for response_id_factory (lines 92-101) says id backends like Foundry storage embed partition keys in the id. We pass the user-supplied previous_response_id straight to the factory so the new record co-locates with the existing partition. Have we thought about what happens when a caller supplies an arbitrary resp_* id pointing at someone else's partition? Without a host-side check that the chain is owned by this caller, the factory ends up writing the new record into a partition the caller does not own. Is partition ownership enforced inside the factory itself, or somewhere upstream we are relying on?
| return | ||
|
|
||
| completed_text = accumulated | ||
| report = await self._ctx.deliver_response(request, HostedRunResult(text=accumulated)) |
There was a problem hiding this comment.
What happens to deliver_response and any push-channel fan-out when the stream raises mid-flight? The non-streaming path always calls deliver_response, but here on failure we emit response.failed and return without ever invoking it. The client has already seen partial deltas on the wire, but host-side history, idempotency, and any non-originating push targets see nothing for this turn. Next turn the chain anchored on this response_id will be inconsistent with what the user actually saw. Should we be calling deliver_response with accumulated (or with an explicit failed marker) before returning, so host state matches wire state?
| "frequency_penalty", | ||
| "presence_penalty", | ||
| "logit_bias", | ||
| "instructions", |
There was a problem hiding this comment.
instructions is in _RESPONSES_OPTION_PASSTHROUGH but parse_responses_request short-circuits on it at line 218 (if key == "instructions": continue) because it is already prepended as a system message. Doesn't that mean the entry here is dead? Doesn't look like a bug today, but the next person who adds another consumer of _RESPONSES_OPTION_PASSTHROUGH will quietly ship instructions twice (system message and option)? Can we drop it from the set?
Motivation and Context
Implements §7 ("Built-in channels — Responses") of SPEC-002 (merged via #5549). Adds the OpenAI Responses-compatible channel so an
AgentFrameworkHostcan be reached by any Responses SDK / OpenAI client.Description
Adds the new
agent-framework-hosting-responsespackage (python/packages/hosting-responses/) with:ResponsesChannel— mountsPOST /responses(configurable), parses the OpenAI Responses request shape intoChannelRequest, and serializes the agent's reply back into a Responses payload (sync or streaming SSE)._parsing.py— request/response translation, isolation-key derivation fromprevious_response_id/safety_identifier, and option scrubbing seam.Stack
PR-3 of 9. Depends on #PR-2 (
feat/hosting-core). Until PR-2 merges, this PR's diff includes that commit too — afterwards the diff shrinks to just this package.Contribution Checklist